#include <QtWidgets>

#include "CodeEdit.h"
#include "LineNumberArea.h"
#include "Highlighter.h"
#include "Minimap.h"

#include "simodo/shell/access/LspStructures.h"


CodeEdit::CodeEdit(CodeEditParameters params)
    : _params(params)
{
    highlightCurrentLine();
    setContentsMargins(0,0,0,0);
    setMouseTracking(true);

    if (_params.line_number_area)
        _line_number_area = new LineNumberArea(this);

    if (_params.minimap) {
        _minimap = new Minimap(this);

        connect(_minimap, &Minimap::requestPosition, this, 
                [this](int pos){
                    /// \todo Использование курсора для навигации по миникарте сбрасывает выделение текста.
                    /// Это очень не удобно, когда нужно посмотреть какие в документе есть ещё такие же фрагменты.
                    /// В этом случае миникарта не может быть использована, т.к. теряются выделения.
                    /// Альтернатива - использовать прокрутку колесом мышки, но это не всегда удобно.
                    /// ЗАДАЧА: Наследовать миникарту от QScrollBar (или делегировать) и заменять стандартный 
                    /// вертикальный скролл бар редактора. 

                    QTextCursor cursor = textCursor();

                    cursor.setPosition(pos);
                    setTextCursor(cursor);
                    centerCursor();

                    _minimap->highlightViewportArea(cursor.selectedText(), _find_options);
                });

        setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    }

    _text_block_shift_right_action = new QAction(tr("Tab current block to right"), this);
    _text_block_shift_right_action->setShortcut(Qt::Key_Tab);
    _text_block_shift_right_action->setStatusTip(tr("Tab current block to right"));
    connect(_text_block_shift_right_action, &QAction::triggered, this,
            [this](bool )
            {
                performBlockShift(ShiftDirection::Right);
            });
    addAction(_text_block_shift_right_action);

    _text_block_shift_left_action = new QAction(tr("Tab current block to left"), this);
    _text_block_shift_left_action->setShortcut(Qt::Key_Backtab);
    _text_block_shift_left_action->setStatusTip(tr("Tab current block to left"));
    connect(_text_block_shift_left_action, &QAction::triggered, this,
            [this](bool )
            {
                performBlockShift(ShiftDirection::Left);
            });
    addAction(_text_block_shift_left_action);

    _text_handle_comment_action = new QAction(tr("Comment/uncomment"), this);
    _text_handle_comment_action->setShortcut(Qt::CTRL + Qt::Key_Slash);
    _text_handle_comment_action->setStatusTip(tr("Comment or uncomment current block"));
    connect(_text_handle_comment_action, &QAction::triggered, this,
            [this](bool )
            {
                handleCommenting();
            });
    addAction(_text_handle_comment_action);

    _save_screen_to_image_action = new QAction(tr("Save screen to image"), this);
    connect(_save_screen_to_image_action, &QAction::triggered, this,
            [this](bool )
            {
                emit requestScreenshot();
            });
    addAction(_save_screen_to_image_action);

    _save_text_to_image_action = new QAction(tr("Save text to image"), this);
    connect(_save_text_to_image_action, &QAction::triggered, this,
            [this](bool )
            {
                emit requestSaveAsPicture();
            });
    addAction(_save_text_to_image_action);

    connect(this, &QPlainTextEdit::blockCountChanged, this, 
            [this](int /*newBlockCount*/)
            {
                /// \note Сначала отдельно вычисляем ширину области с номерами строк и только затем
                /// остальное. Последовательность вычисления важна, поэтому не вычисляем при передаче параметров
                /// в метод resetViewportMargins.
                int lna_width = calcLineNumberAreaWidth();
                resetViewportMargins(lna_width, calcMinimapWidth(lna_width));
            });
    connect(this, &QPlainTextEdit::updateRequest, this, &CodeEdit::updateLineNumberArea);
    connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CodeEdit::highlightCurrentLine);

    connect(this, &QPlainTextEdit::selectionChanged, this, 
            [this]()
            {
                highlightCurrentLine();

                if (_minimap)
                    _minimap->highlightViewportArea(textCursor().selectedText(), _find_options);
            });
}

CodeEdit::~CodeEdit()
{
    if (_completer)
        delete _completer;

    if (_line_number_area)
        delete _line_number_area;

    if (_minimap)
        delete _minimap;
}

int CodeEdit::calcLineNumberAreaWidth()
{
    if (!_params.line_number_area)
        return 0;

    int digits = 1;
    int max = qMax(1, blockCount());
    while (max >= 10) {
        max /= 10;
        ++digits;
    }

    int space = fontMetrics().horizontalAdvance(QLatin1Char('9')) * (digits+3);

    return space;
}

int CodeEdit::calcMinimapWidth(int line_number_area_width)
{
    if (!_minimap)
        return 0;

    if (!params().minimap_auto_scaling)
        return 80 * _minimap->font().pointSize();

    if (font().pointSize() <= 0 || _minimap->font().pointSize() <= 0
     || font().pointSize() < _minimap->font().pointSize()) {
        qDebug() << QString("CodeEdit::calcMinimapWidth: font size dysfunction!");
        return 80 * _minimap->font().pointSize();
     }

    QRect cr = contentsRect();

    /// \todo При вычислении ширины миникарты какие-то параметры не учитываются, из-за чего возникает погрешность.
    /// Нужно найти неучитываемые параметры

    // Исходим из пропорции и известного равенства:
    // edit_width / minimap_width = edit_font_size / minimap_font_size
    // edit_width = cr.width() - line_number_area_width - minimap_width 

    // Сводим в одну формулу:
    // (cr.width() - line_number_area_width - minimap_width) / minimap_width = edit_font_size / minimap_font_size
    
    // Получается:
    // cr.width() - line_number_area_width - minimap_width = ( edit_font_size / minimap_font_size ) * minimap_width
    // или
    // cr.width() - line_number_area_width - minimap_width - ( edit_font_size / minimap_font_size ) * minimap_width = 0
    // или
    // a - x - b x = 0 
    // x + b x - a = 0
    // x (1+b) - a = 0
    // x = a / (1+b)
    // minimap_width = ( cr.width() - line_number_area_width ) / (1 + edit_font_size / minimap_font_size)

    const QString sample_text = "// Начальное положение на ВПП XG = -4.0; модель(DELTA_T);";

    double edit_font_size    = QFontMetricsF(font()).horizontalAdvance(sample_text);
    double minimap_font_size = QFontMetricsF(_minimap->font()).horizontalAdvance(sample_text);
    double a                 = cr.width() - line_number_area_width;
    double b                 = edit_font_size / minimap_font_size;

    double minimap_width     = a / (1 + b);

    return qRound(minimap_width); 
}

void CodeEdit::resetViewportMargins(int line_number_area_width, int minimap_width)
{
    if (line_number_area_width == _last_line_number_area_width && minimap_width == _last_minimap_width)
        return;

    setViewportMargins(line_number_area_width, 0, minimap_width, 0);

    _last_line_number_area_width = line_number_area_width;
    _last_minimap_width = minimap_width;
}

void CodeEdit::updateLineNumberArea(const QRect &rect, int dy)
{
    if (!_params.line_number_area)
        return;

    if (dy)
        _line_number_area->scroll(0, dy);
    else
        _line_number_area->update(0, rect.y(), _line_number_area->width(), rect.height());

    if (rect.contains(viewport()->rect())) {
        /// \note Сначала отдельно вычисляем ширину области с номерами строк и только затем
        /// остальное. Последовательность вычисления важна, поэтому не вычисляем при передачи параметров
        /// в метод resetViewportMargins.
        int lna_width = calcLineNumberAreaWidth();
        resetViewportMargins(lna_width, calcMinimapWidth(lna_width));
    }
}

void CodeEdit::contentsChange_slot(int position, int charsRemoved, int charsAdded)
{
    _mm_position     = position;
    _mm_chars_removed = charsRemoved;
    _mm_chars_added   = charsAdded;
    /// \todo Нужно использовать этот приём для обновления текста документа на языковом сервере
    // qDebug() << QString("QTextDocument::contentsChange: position = %1, charsRemoved = %2, charsAdded = %3")
    //                     .arg(position)
    //                     .arg(charsRemoved)
    //                     .arg(charsAdded);
}

void CodeEdit::resetHighlighter(Highlighter * highlighter)
{
    _highlighter = highlighter;

    if (_minimap)
        _minimap->resetHighlighter(highlighter);
}

void CodeEdit::rehighlight()
{
    if (_highlighter)
        _highlighter->rehighlight();

    if (_minimap)
        _minimap->rehighlight();
}

bool CodeEdit::resetCompleter(QAbstractItemModel *model, 
                              const std::vector<std::u16string> & trigger_characters,
                              QAbstractItemView * popup)
{
    if (_completer)
        delete _completer;

    _trigger_characters.clear();
    for(const std::u16string & s : trigger_characters)
        _trigger_characters.push_back(QString::fromStdU16String(s));

    if (_params.use_completer) {
        _completer = new QCompleter(this);

        if (popup)
            _completer->setPopup(popup);

        _completer->setWidget(this);
        _completer->setModel(model);
        _completer->setCompletionMode(QCompleter::PopupCompletion);
        _completer->setCaseSensitivity(Qt::CaseInsensitive);
        _completer->setFilterMode(Qt::MatchContains);
        _completer->setMaxVisibleItems(_params.completer_max_rows);

        QObject::connect(_completer, QOverload<const QString &>::of(&QCompleter::activated),
                        this, [this](const QString & completion){
                            QTextCursor tc = selectWord();
                            tc.insertText(completion);
                            setTextCursor(tc);
                        });
        return true;
    }

    _completer = nullptr;
    return false;
}

QTextCursor CodeEdit::selectWord() const
{
    QTextCursor tc          = textCursor();
    QString     text        = tc.block().text();
    int         position    = tc.positionInBlock();
    int         word_begin_index = position, 
                word_end_index   = position;

    for(; word_begin_index > 0; --word_begin_index) 
        if (QString(" \t").contains(text[word_begin_index-1]) || end_of_word.contains(text[word_begin_index-1]))
            break;
    
    if (!_params.ignore_right_side_of_word)
        for(; word_end_index < text.length(); ++word_end_index) 
            if (QString(" \t").contains(text[word_end_index]) || end_of_word.contains(text[word_end_index]))
                break;
        
    if (word_end_index <= word_begin_index)
        return tc;

    if (word_begin_index < position)
        tc.movePosition(QTextCursor::Left, QTextCursor::MoveMode::MoveAnchor, position-word_begin_index);

    tc.movePosition(QTextCursor::Right, QTextCursor::MoveMode::KeepAnchor, word_end_index-word_begin_index);

    return tc;
}

QTextCursor CodeEdit::selectLeftPunctuation() const
{
    QTextCursor tc          = textCursor();
    QString     text        = tc.block().text();
    int         position    = tc.positionInBlock();
    int         punctuation_end_index = position, 
                punctuation_begin_index;

    for(; punctuation_end_index > 0; --punctuation_end_index) 
        if (end_of_word.contains(text[punctuation_end_index-1]))
            for(punctuation_begin_index=punctuation_end_index-1; punctuation_begin_index >= 0; --punctuation_begin_index)
                if (punctuation_begin_index == 0 || !end_of_word.contains(text[punctuation_begin_index-1])) {
                    tc.movePosition(QTextCursor::Left, QTextCursor::MoveMode::MoveAnchor, 
                                    position-punctuation_begin_index);
                    tc.movePosition(QTextCursor::Right, QTextCursor::MoveMode::KeepAnchor, 
                                    punctuation_end_index-punctuation_begin_index);
                    return tc;
                }

    return tc;
}

void CodeEdit::showCompleterPopup()
{
    _completer->popup()->setFont(font());
    QRect completer_rect = cursorRect();
    completer_rect.setX(completer_rect.x() + calcLineNumberAreaWidth());
    completer_rect.setWidth(_completer->popup()->sizeHintForColumn(0) + _completer->popup()->verticalScrollBar()->sizeHint().width());
    _completer->complete(completer_rect); // popup it up!
}

void CodeEdit::resetMinimap()
{
    if (!_minimap)
        return;

    /// \note Большие файлы могут тормозить, поэтому проверяем и удаляем миникарту, если строк слишком много
    if (document()->lineCount() > 100000) {
        delete _minimap;
        _minimap = nullptr;

        setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
        return;
    }

    QFont f = font();
    f.setPointSize(params().minimap_font_point_size);

    _minimap->setFont(f);
    _minimap->setLineWrapMode(_params.wrap_lines
                                ? QPlainTextEdit::LineWrapMode::WidgetWidth
                                : QPlainTextEdit::LineWrapMode::NoWrap);
    _minimap->setPlainText(toPlainText());
    _minimap->setTabStopDistance(QFontMetricsF(_minimap->font()).horizontalAdvance(' ') * params().tab_size);
    /// \note Сначала отдельно вычисляем ширину области с номерами строк и только затем
    /// остальное. Последовательность вычисления важна, поэтому не вычисляем при передачи параметров
    /// в метод resetViewportMargins.
    int lna_width = calcLineNumberAreaWidth();
    resetViewportMargins(lna_width, calcMinimapWidth(lna_width));

    connect(document(), &QTextDocument::contentsChange, this, &CodeEdit::contentsChange_slot);
    connect(document(), &QTextDocument::contentsChanged, this,
            [this]{
                if (_minimap && (_mm_chars_removed != 0 || _mm_chars_added != 0)) 
                {
                    QTextCursor cursor = _minimap->textCursor();
                    cursor.setPosition(_mm_position);
                    if (_mm_chars_removed != 0)
                        cursor.setPosition(_mm_position+_mm_chars_removed, QTextCursor::KeepAnchor);

                    if (_mm_chars_added != 0) {
                        QTextCursor mine = textCursor();
                        mine.setPosition(_mm_position);
                        mine.setPosition(_mm_position+_mm_chars_added, QTextCursor::KeepAnchor);
                        cursor.insertText(mine.selectedText());
                    }
                    else 
                        cursor.removeSelectedText();

                    _mm_chars_removed = 0;
                    _mm_chars_added   = 0;
    
                    _minimap->highlightViewportArea(textCursor().selectedText(), _find_options);
                }
            });
    connect(this, &QPlainTextEdit::cursorPositionChanged, this, 
            [this]{
                if (_minimap)
                    _minimap->highlightViewportArea(textCursor().selectedText(), _find_options);  
            });
}

void CodeEdit::resetFont(QFont f)
{
    setFont(f);
    setTabStopDistance(QFontMetricsF(font()).horizontalAdvance(' ') * params().tab_size);

    if (_minimap) {
        /// \note Сначала отдельно вычисляем ширину области с номерами строк и только затем
        /// остальное. Последовательность вычисления важна, поэтому не вычисляем при передачи параметров
        /// в метод resetViewportMargins.
        int lna_width = calcLineNumberAreaWidth();
        resetViewportMargins(lna_width, calcMinimapWidth(lna_width));
        _minimap->highlightViewportArea(textCursor().selectedText(), _find_options);
    }
}

void CodeEdit::setLineCol(QPair<int,int> line_col)
{
    QTextCursor cursor = textCursor();
    cursor.setPosition(
        document()->findBlockByNumber(line_col.first-1).position()
        + line_col.second-1);
    setTextCursor(cursor);
    centerCursor();

    if (_minimap)
        _minimap->highlightViewportArea(textCursor().selectedText(), _find_options);  

}

QString CodeEdit::getErrorText(int line, int character)
{
    QString text;
    if (_highlighter)
        for(const simodo::shell::Diagnostic & d : _highlighter->diagnostics())
            if (d.range.start().line() <= simodo::inout::position_line_t(line) && d.range.end().line() >= simodo::inout::position_character_t(line))
                if (character < 0 || d.range.contains(simodo::inout::Position(line,character))) {
                    text += "<p>";
                    QString severity_level_name = QString::fromStdU16String(simodo::lsp::getDiagnosticSeverityString(d.severity));
                    if (!severity_level_name.isEmpty())
                        text += "<b>" + severity_level_name + ": </b>";
                    /// \note Длинные пояснения могут не умещаться на экране, если сообщений для конкретной строки много.
                    /// Поэтому берём только первое сообщение до перевода каретки. Остальные можно посмотреть в ToolTip 
                    /// списка под окном.
                    // QString message = d.message;
                    // text += message.replace("\n","</p><p>") + "</p>";
                    text += d.message.split("\n").front() + "</p>";
                }

    return text;
}

void CodeEdit::hover(QString text)
{
    if (!QToolTip::isVisible())
        return;

    if (text.isEmpty() && _tooltip_text.isEmpty()) {
        QToolTip::hideText();
        return;
    }

    if (text.isEmpty())
        return;

    if (!_tooltip_text.isEmpty())
        text = "<p>" + text.replace("\n","<br>") + "</p>" + _tooltip_text;
    QToolTip::showText(_tooltip_pos, text);
}

QPixmap CodeEdit::toPixmap()
{
    QTextBlock block     = document()->firstBlock();
    int        top       = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
    int        h         = top;
    int        lna_width = calcLineNumberAreaWidth();
    int        w         = width() - lna_width - calcMinimapWidth(lna_width);

    while(block.isValid()) {
        h += qRound(blockBoundingRect(block).height());
        block = block.next();
    }

    QPixmap     map(w, h);

    map.fill(Qt::transparent);

    QPainter    painter(&map);
    int         offset = top;

    block = document()->firstBlock();

    while(block.isValid()) {

        int shift = qRound(blockBoundingRect(block).height());

        if (offset+shift > h)
            break;

        if (block.isVisible()) {
            QTextLayout * layout = block.layout();

            layout->draw(&painter, QPoint(0,offset));
        }

        offset += shift;
        block = block.next();
    }

    return map;
}

QPixmap CodeEdit::getScreenshot()
{
    QTextBlock block     = firstVisibleBlock();
    int        top       = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
    int        h         = height();
    int        lna_width = calcLineNumberAreaWidth();
    int        w         = width() - lna_width - calcMinimapWidth(lna_width);

    QPixmap     map(w, h);

    map.fill(Qt::transparent);

    QPainter    painter(&map);
    int         offset = top;

    block = firstVisibleBlock();

    while(block.isValid()) {

        int shift = qRound(blockBoundingRect(block).height());

        if (offset+shift > h)
            break;

        if (block.isVisible()) {
            QTextLayout * layout = block.layout();

            layout->draw(&painter, QPoint(0,offset));
        }

        offset += shift;
        block = block.next();
    }

    return map;
}

QPair<int,int> CodeEdit::getViewportLines()
{
    QTextBlock block  = firstVisibleBlock();
    int        first  = block.blockNumber();
    int        last   = first;
    int        top    = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
    int        h      = height();
    int        offset = top;

    while(block.isValid()) {
        int shift = qRound(blockBoundingRect(block).height());

        if (offset+shift > h)
            break;

        last = block.blockNumber();

        offset += shift;
        block = block.next();
    }

    return {first, last};
}

void CodeEdit::nearToClose()
{
    disconnect(document(), &QTextDocument::contentsChange, this, &CodeEdit::contentsChange_slot);
}

void CodeEdit::highlightCurrentLine()
{
    QList<QTextEdit::ExtraSelection> extraSelections;

    if (!isReadOnly()) {
        QTextEdit::ExtraSelection selection;

        QColor line_color = QColor(palette().color(backgroundRole())).lighter(100);

        selection.format.setBackground(line_color);
        selection.format.setProperty(QTextFormat::FullWidthSelection, true);
        selection.cursor = textCursor();
        selection.cursor.clearSelection();
        extraSelections.append(selection);
    }

    QTextCursor cursor = textCursor();
    QString     select = cursor.selectedText();

    /// \todo Добавить в настройки!
    int         selection_min_length = 1;

    if (select.length() >= selection_min_length) {
        QTextCharFormat format;

        /// \todo Нужно подобрать хорошие цвета для выделений из палитры или протащить их из настроек
        format.setBackground(palette().brush(QPalette::Normal, QPalette::Highlight));
        format.setForeground(palette().brush(QPalette::Normal, QPalette::HighlightedText));

        QTextDocument * doc = document();
        QTextCursor     cur = doc->find(select, 0, _find_options);
        int             qua = 0;
        /// \todo Добавить в настройки!
        int             lim = 1000;

        while(!cur.isNull() && qua < lim) {
            QTextEdit::ExtraSelection selection;

            selection.cursor = cur;
            selection.format = format;

            if (cur != cursor)
                extraSelections.append(selection);

            cur = doc->find(select, cur, _find_options);
            qua ++;
        }
    }

    setExtraSelections(extraSelections);
}

void CodeEdit::resizeEvent(QResizeEvent *e)
{
    QPlainTextEdit::resizeEvent(e);

    QRect cr        = contentsRect();
    int   lna_width = calcLineNumberAreaWidth();

    if (_line_number_area)
        _line_number_area->setGeometry(QRect(cr.left(), cr.top(), lna_width, cr.height()));
    
    if (_minimap) {
        /// \todo Можно совмещать миникарту и вертикальную полосу прокрутки.
        /// Тогда закомментированный код может пригодиться.
        // QScrollBar * vertical_scroll_bar = verticalScrollBar();
        // int          vertical_scroll_bar_width = vertical_scroll_bar ? vertical_scroll_bar->width() : 0;

        // _minimap->setGeometry(QRect(cr.width()-minimap_width()-vertical_scroll_bar_width, 
        //                       cr.top(), 
        //                       minimap_width()+vertical_scroll_bar_width, 
        //                       cr.height()));

        int mm_width = calcMinimapWidth(lna_width);
        resetViewportMargins(lna_width, mm_width);
        _minimap->setGeometry(QRect(cr.left()+cr.width()-mm_width, cr.top(), mm_width, cr.height()));
        _minimap->highlightViewportArea(textCursor().selectedText(), _find_options);
    }
}

void CodeEdit::focusInEvent(QFocusEvent * event)
{
    QPlainTextEdit::focusInEvent(event);

    if(event->gotFocus()) 
        emit gotFocus();
}

void CodeEdit::focusOutEvent(QFocusEvent * event)
{
    QPlainTextEdit::focusOutEvent(event);

    if (event->lostFocus()) {
        emit lostFocus();
    }
}

void CodeEdit::wheelEvent(QWheelEvent * event)
{
    if (event->modifiers() != Qt::ControlModifier)
        QPlainTextEdit::wheelEvent(event);
}

bool CodeEdit::event(QEvent * event)
{
    if (event->type() == QEvent::ToolTip && _params.lsp_support) {
        QHelpEvent *    e           = static_cast<QHelpEvent *>(event);
        QPoint          pos         = e->pos();
        int             char_width  = fontMetrics().horizontalAdvance(QLatin1Char('9'));
        int             x           = pos.x() - calcLineNumberAreaWidth() - char_width;

        pos.setX(qMax(0,x));

        QTextCursor     cursor       = cursorForPosition(pos);
        int             line         = cursor.blockNumber();
        int             character    = std::max(0, cursor.positionInBlock());
        QString         tooltip_text = getErrorText(line, character);

        if (character >= cursor.block().text().length() || cursor.block().text()[character] == ' ')
            return true;

        _tooltip_text = tooltip_text;
        _tooltip_pos  = e->globalPos();

        cursor.select(QTextCursor::WordUnderCursor);
        QString         selected_text = cursor.selectedText();
        if (_params.lsp_hover && !selected_text.isEmpty() && x >= 0 && x < cursor.block().length()*char_width)
            if (!_params.hover_for_identifiers_only || !not_front_of_word.contains(selected_text[0])) {
                if (selected_text != _last_hover_word) {
                    emit requestHover(line, character);

                    /// @note Запоминать последнее слово, для которого запрашивается (и возможно показывается) подсказка
                    /// нужно для того, чтобы не спамить LSP-сервер одинаковыми запросами. Однако возникает ситуация, когда
                    /// курсор мышки может быть наведён на такое же слово, но в другом контексте, или на тоже слово, но чуть позже
                    /// - в этих случаях подсказка не будет показана (изменена). Для предотвращения такого поведения добавлен
                    /// таймер устаревания этого слова, чтобы сработал перезапрос на подсказку.
                    _last_hover_word = selected_text;
                    _last_hover_word_expired_timer = startTimer(2000);

                    if (tooltip_text.isEmpty())
                        tooltip_text = tr("Searching for '%1'...").arg(selected_text);
                }
                else
                    tooltip_text = QToolTip::text();
            }

        if (tooltip_text.isEmpty()) {
            QToolTip::hideText();
            event->ignore();
        }
        else
            QToolTip::showText(_tooltip_pos, tooltip_text);

        return true;
    }

    if (event->type() == QEvent::PaletteChange)
        if (_highlighter) {
            _highlighter->setupCurrentFormat(palette());
            rehighlight();
        }

    return QPlainTextEdit::event(event);
}

void CodeEdit::timerEvent(QTimerEvent * event)
{
    if (event->timerId() == _last_hover_word_expired_timer) {
        killTimer(_last_hover_word_expired_timer);
        _last_hover_word.clear();
    }
}

void CodeEdit::paintEvent(QPaintEvent *event)
{
    QPlainTextEdit::paintEvent(event);

    highlightCurrentLine();
}

void CodeEdit::contextMenuEvent(QContextMenuEvent *e)
{
    QMenu *     menu    = createStandardContextMenu();
    QTextCursor cursor  = textCursor();

    _text_block_shift_right_action->setEnabled(cursor.selectionStart() != cursor.selectionEnd());
    _text_block_shift_left_action->setEnabled(cursor.selectionStart() != cursor.selectionEnd());

    menu->addSeparator();
    menu->addAction(_text_block_shift_right_action);
    menu->addAction(_text_block_shift_left_action);
    menu->addAction(_text_handle_comment_action);
    menu->addSeparator();
    menu->addAction(_save_screen_to_image_action);
    menu->addAction(_save_text_to_image_action);

    menu->exec(e->globalPos());
    delete menu;
}

void CodeEdit::keyPressEvent(QKeyEvent *event)
{
    if (_completer && _completer->popup()->isVisible()) {
        // The following keys are forwarded by the completer to the widget
       switch (event->key()) {
       case Qt::Key_Enter:
       case Qt::Key_Return:
       case Qt::Key_Escape:
       case Qt::Key_Tab:
       case Qt::Key_Backtab:
            event->ignore();
            return; // let the completer do default behavior
       default:
           break;
       }
    }

    if (event->key() == Qt::Key_Tab && event->modifiers() == Qt::NoModifier) {
        if (textCursor().selectionStart() == textCursor().selectionEnd()) {
            if (_params.tabs_replacing) {
                performTab();
                event->ignore();
                return;
            }
        }
        else {
            performBlockShift(ShiftDirection::Right);
            event->ignore();
            return;
        }
    }
    else if (event->key() == Qt::Key_Backtab) {
        performBlockShift(ShiftDirection::Left);
        event->ignore();
        return;
    }
    else if (event->modifiers().testFlag(Qt::ControlModifier) && event->key() == Qt::Key_Slash) {
        handleCommenting();
        event->ignore();
        return;
    }
    else if ((event->key() == Qt::Key_Return) 
     && event->modifiers() == Qt::NoModifier
     && _params.auto_indent) {
        performAutoIndent();
        event->ignore();
        return;
    }
    else if (event->key() == Qt::Key_F12) {
        QTextCursor     cursor    = textCursor();
        int             line      = cursor.blockNumber();
        int             character = cursor.positionInBlock();

        if (event->modifiers().testFlag(Qt::ShiftModifier))
            emit requestDeclaration(line, character);
        else if (event->modifiers() == Qt::NoModifier)
            emit requestDefinition(line, character);

        event->ignore();
        return;
    }

    /// @note Комплитер можно активировать по сочетанию клавиш Ctrl+Space, которая задана 
    /// жёстко. Вопрос: насколько данная комбинация распространена и уместна,
    /// и нужно ли делать её настраиваемой?

    bool isShortcut = (event->modifiers().testFlag(Qt::ControlModifier) && event->key() == Qt::Key_Space);
    if (!_completer || !isShortcut) // do not process the shortcut when we have a completer
        QPlainTextEdit::keyPressEvent(event);

    if (!_completer)
        return;

    QTextCursor     cursor    = textCursor();
    
    if (_trigger_characters.contains(event->text())) {
        emit requestForceCompletion();
        isShortcut = true;
    }

    const bool ctrlOrShift = event->modifiers().testFlag(Qt::ControlModifier) ||
                             event->modifiers().testFlag(Qt::ShiftModifier);
    if (ctrlOrShift && event->text().isEmpty())
        return;

    const bool     hasModifier       = (event->modifiers() != Qt::NoModifier) && !ctrlOrShift;
    QString        completionPrefix  = selectWord().selectedText();

    if (!isShortcut && (hasModifier || event->text().isEmpty()
            || (_completer->popup()->isHidden() && completionPrefix.length() < _params.chars_count_for_completer_popup)
            || end_of_word.contains(event->text().right(1))
            )
        ) {
        _completer->popup()->hide();
        return;
    }

    if (completionPrefix != _completer->completionPrefix()) {
        _completer->setCompletionPrefix(completionPrefix);
        _completer->popup()->setCurrentIndex(_completer->completionModel()->index(0, 0));
    }
    showCompleterPopup();
}

void CodeEdit::mousePressEvent(QMouseEvent * event)
{
    if (event->button() & Qt::LeftButton
     && (event->modifiers().testFlag(Qt::ControlModifier) || event->modifiers().testFlag(Qt::ShiftModifier))) {
        QTextCursor cursor = cursorForPosition(event->pos());

        _clicked_line = cursor.blockNumber();
        _clicked_character = cursor.positionInBlock();
    }

    QPlainTextEdit::mousePressEvent(event);
}

void CodeEdit::mouseReleaseEvent(QMouseEvent * event)
{
    if (event->button() & Qt::LeftButton
     && (event->modifiers().testFlag(Qt::ControlModifier) || event->modifiers().testFlag(Qt::ShiftModifier))) {
        QTextCursor     cursor    = cursorForPosition(event->pos());
        int             line      = cursor.blockNumber();
        int             character = cursor.positionInBlock();

        if (line == _clicked_line && character == _clicked_character) {
            if (event->modifiers().testFlag(Qt::ShiftModifier))
                emit requestDeclaration(line, character);
            else 
                emit requestDefinition(line, character);
        }
    }

    QPlainTextEdit::mouseReleaseEvent(event);
}

void CodeEdit::mouseMoveEvent(QMouseEvent * event)
{
    static bool overridden = false;

    if (_highlighter) {
        if (event->modifiers().testFlag(Qt::ControlModifier)) {
            QPoint          pos     = event->pos();
            QTextCursor     cursor  = cursorForPosition(pos);
            int             line      = cursor.blockNumber();
            int             character = cursor.positionInBlock();

            const std::multimap<int,simodo::shell::SemanticToken> & tokens = _highlighter->semantic_tokens();

            auto range = tokens.equal_range(line);
            for(auto it = range.first; it != range.second; ++it) {
                const simodo::shell::SemanticToken & st = it->second;
                if (st.startChar <= character && st.startChar+st.length >= character)
                    if (_reference_line != line
                     || _reference_character != character
                     || _reference_length != st.length) {
                        const std::vector<std::u16string> & mods = st.tokenModifiers;
                        for(const std::u16string & m : mods)
                            if (m == u"anchor") {
                                int last_reference_line = _reference_line;

                                _reference_line = line;
                                _reference_character = st.startChar;
                                _reference_length = st.length;

                                _highlighter->setAnchorPosition({_reference_line, _reference_character, _reference_length});

                                if (!overridden) {
                                    overridden = true;
                                    QApplication::setOverrideCursor(Qt::PointingHandCursor);
                                }

                                if (_reference_line >= 0) 
                                    _highlighter->rehighlightBlock(document()->findBlockByNumber(last_reference_line));

                                _highlighter->rehighlightBlock(cursor.block());
        
                                QPlainTextEdit::mouseMoveEvent(event);
                                return;
                            }
                    }
            }
        }

        if (overridden) {
            overridden = false;
            QApplication::restoreOverrideCursor();
        }

        if (_reference_length > 0) {
            // setCursor(Qt::ArrowCursor);
            QTextBlock block = document()->findBlockByNumber(_reference_line);

            _reference_line = -1;
            _reference_character = -1;
            _reference_length = 0;

            _highlighter->setAnchorPosition({_reference_line, _reference_character, _reference_length});
            _highlighter->rehighlightBlock(block);
        }
    }

    QPlainTextEdit::mouseMoveEvent(event);
}

void CodeEdit::scrollContentsBy(int dx, int dy)
{
    QPlainTextEdit::scrollContentsBy(dx, dy);

    if (_minimap)
        _minimap->highlightViewportArea(textCursor().selectedText(), _find_options);
}

int CodeEdit::shiftRight(QTextCursor &cursor, int end_selection)
{
    if (_params.tabs_replacing) {
        cursor.insertText(QString(_params.tab_size, ' '));
        end_selection += _params.tab_size;
    }
    else {
        cursor.insertText("\t");
        end_selection ++;
    }

    return end_selection;
}

int CodeEdit::shiftLeft(QTextCursor &cursor, int end_selection)
{
    QString text = cursor.block().text();
    int     i    = 0;

    for(; i < text.size(); ++i)
        if (i == _params.tab_size)
            break;
        else if (text[i] != ' ') {
            if (text[i] == '\t')
                i++;
            break;
        }

    if (i > 0) {
        cursor.setPosition(cursor.position()+i, QTextCursor::KeepAnchor);
        cursor.removeSelectedText();
        end_selection -= i;
    }

    return end_selection;
}

void CodeEdit::performBlockShift(ShiftDirection direction)
{
    QTextCursor cursor          = textCursor();
    int         start_sel       = cursor.selectionStart();
    int         end_sel         = cursor.selectionEnd();
    int         init_position   = cursor.position();
    int         init_anchor     = cursor.anchor();

    if (start_sel >= end_sel)
        return;

    cursor.beginEditBlock();
    cursor.setPosition(start_sel);
    cursor.movePosition(QTextCursor::StartOfLine);

    do {
        end_sel = (direction == ShiftDirection::Right) ? shiftRight(cursor, end_sel) : shiftLeft(cursor, end_sel);

        if (!cursor.movePosition(QTextCursor::QTextCursor::NextBlock))
            break;
    }
    while(cursor.position() < end_sel);

    end_sel = std::min(end_sel,cursor.position());

    cursor.setPosition((init_position > init_anchor) ? init_anchor : end_sel);
    cursor.setPosition((init_position > init_anchor) ? end_sel : init_position, QTextCursor::KeepAnchor);
    cursor.endEditBlock();
    setTextCursor(cursor);
}

void CodeEdit::performAutoIndent()
{
    QTextCursor cursor      = textCursor();
    QTextBlock  block       = cursor.block();
    QString     block_str   = block.text();
    int         pos_inline  = cursor.position() - block.position();
    QString     indent_str;

    if (pos_inline < block_str.size())
        for(int i=0; i < pos_inline; ++i) {
            auto ch = block_str[i];

            if (ch != ' ' && ch != '\t')
                break;

            indent_str += ch;
        }
    else
        while(true) {
            int i = 0;
            for(; i < pos_inline; ++i) {
                auto ch = block_str[i];

                if (ch != ' ' && ch != '\t')
                    break;

                indent_str += ch;
            }

            if (i < pos_inline)
                break;

            indent_str.clear();

            block = block.previous();

            if (!block.isValid())
                break;

            block_str   = block.text();
            pos_inline  = block_str.size();
        }

    insertPlainText("\n" + indent_str);
}

void CodeEdit::performTab()
{
    QTextCursor cursor = textCursor();

    cursor.beginEditBlock();
    performTabReplacement(cursor);

    int cursor_last_position = cursor.position();
    int line_position        = cursor.block().position();
    int shift                = _params.tab_size - (cursor_last_position - line_position) % _params.tab_size;

    cursor.insertText(QString(shift, ' '));

    cursor.endEditBlock();
    setTextCursor(cursor);
}

int CodeEdit::performTabReplacement(QTextCursor &cursor)
{
    int cursor_last_position = cursor.position();
    int line_position        = cursor.block().position();
    int position             = line_position;
    int changed              = 0;

    // replace tab to spaces in whole line

    while(true) {
        int     i    = position - line_position;
        QString text = cursor.block().text();

        if (i >= text.size())
            break;

        if (text[i] == '\t') {
            int shift = _params.tab_size - i % _params.tab_size;

            cursor.setPosition(position);
            cursor.setPosition(position + 1, QTextCursor::KeepAnchor);
            cursor.insertText(QString(shift, ' '));
            if (position < cursor_last_position)
                cursor_last_position += shift-1;
            position += shift;
            changed += shift-1;
        }
        else
            position ++;
    }

    // remove last spaces

    int     space_count = 0;
    QString text        = cursor.block().text();

    position = line_position + text.size();

    for(; space_count < text.size(); ++space_count) {
        if (position-space_count <= cursor_last_position)
            break;

        if (text[text.size()-space_count-1] != ' ')
            break;
    }

    if (space_count > 0) {
        cursor.setPosition(position);
        cursor.setPosition(position - space_count, QTextCursor::KeepAnchor);
        cursor.removeSelectedText();
        changed -= space_count;
    }

    cursor.setPosition(cursor_last_position);

    return changed;
}

void CodeEdit::handleCommenting()
{
    if (!_params.comment_by_slash
     || (_params.line_comment_start_chars.isEmpty() && _params.comment_begin_chars.isEmpty()))
        return;

    QString     comment_line_start      = _params.line_comment_start_chars;
    QString     comment_multiline_start = _params.comment_begin_chars;
    QString     comment_multiline_end   = _params.comment_finish_chars;
    QTextCursor cursor          = textCursor();
    int         start_sel       = cursor.selectionStart();
    int         end_sel         = cursor.selectionEnd();
    int         init_position   = cursor.position();
    int         init_anchor     = cursor.anchor();

    cursor.setPosition(start_sel);

    if (!comment_multiline_start.isEmpty() && init_position != init_anchor) {
        int ind = cursor.block().text().indexOf(comment_multiline_start, start_sel - cursor.block().position());

        if (ind == start_sel - cursor.block().position()) {
            //TODO: crush if end_sel-comment_multiline_end.size() < 0, but unreal.
            int pos_left_comment = end_sel - comment_multiline_end.size();

            cursor.setPosition(pos_left_comment);

            int ind = cursor.block().text().indexOf(comment_multiline_end, pos_left_comment - cursor.block().position());

            if (ind == pos_left_comment - cursor.block().position()) {
                // match!
                cursor.beginEditBlock();
                cursor.setPosition(start_sel);
                cursor.setPosition(start_sel+comment_multiline_start.size(),  QTextCursor::KeepAnchor);
                cursor.removeSelectedText();
                end_sel-= comment_multiline_start.size();
                cursor.setPosition(end_sel-comment_multiline_end.size());
                cursor.setPosition(end_sel,  QTextCursor::KeepAnchor);
                cursor.removeSelectedText();
                end_sel-= comment_multiline_end.size();

                cursor.setPosition((init_position > init_anchor) ? init_anchor : end_sel);
                cursor.setPosition((init_position > init_anchor) ? end_sel : init_position, QTextCursor::KeepAnchor);
                cursor.endEditBlock();
                setTextCursor(cursor);

                return;
            }
        }
    }

    cursor.setPosition(end_sel);

    int end_block_position = cursor.block().position();

    cursor.setPosition(start_sel);

    int start_block_position = cursor.block().position();

    if (!comment_line_start.isEmpty()
     && (start_sel == end_sel || (start_sel == start_block_position && end_sel == end_block_position))) {
        int delta = 0;

        cursor.beginEditBlock();
        cursor.movePosition(QTextCursor::StartOfLine);

        do {
            if (!cursor.block().text().isEmpty()) {
                if (delta == 0)
                    delta = (cursor.block().text().startsWith(comment_line_start))
                            ? -comment_line_start.size()
                            : comment_line_start.size();

                if (delta < 0) {
                    if (cursor.block().text().startsWith(comment_line_start)) {
                        cursor.setPosition(cursor.block().position()-delta, QTextCursor::KeepAnchor);
                        cursor.removeSelectedText();
                        end_sel += delta;
                    }
                }
                else {
                    cursor.insertText(comment_line_start);
                    end_sel += delta;
                }
            }

            if (!cursor.movePosition(QTextCursor::QTextCursor::NextBlock))
                break;
        }
        while(cursor.position() < end_sel);

        if (init_position == init_anchor)
            cursor.setPosition(end_sel);
        else {
            cursor.setPosition((init_position > init_anchor) ? init_anchor : end_sel);
            cursor.setPosition((init_position > init_anchor) ? end_sel : init_position, QTextCursor::KeepAnchor);
        }
        cursor.endEditBlock();
        setTextCursor(cursor);

        return;
    }

    if (!comment_multiline_start.isEmpty() && init_position != init_anchor) {
        cursor.beginEditBlock();
        cursor.setPosition(start_sel);
        cursor.insertText(comment_multiline_start);
        end_sel += comment_multiline_start.size();
        cursor.setPosition(end_sel);
        cursor.insertText(comment_multiline_end);
        end_sel += comment_multiline_end.size();

        cursor.setPosition((init_position > init_anchor) ? init_anchor : end_sel);
        cursor.setPosition((init_position > init_anchor) ? end_sel : init_position, QTextCursor::KeepAnchor);
        cursor.endEditBlock();
        setTextCursor(cursor);
    }
}


